// eslint-disable-next-line no-unused-vars
(function(wsp) {
  var Gun = require('../gun'),
    ws = require('ws').Server,
    http = require('./http'),
    url = require('url');
  Gun.on('opt').event(function(gun, opt) {
    gun.__.opt.ws = opt.ws = gun.__.opt.ws || opt.ws || {};
    function start(server, port, app) {
      if (app && app.use) {
        app.use(gun.wsp.server);
      }
      server = gun.__.opt.ws.server =
        gun.__.opt.ws.server || opt.ws.server || server;
      require('./ws')(
        (gun.wsp.ws = gun.wsp.ws || new ws(gun.__.opt.ws)),
        function(req, res) {
          var ws = this;
          req.headers['gun-sid'] = ws.sid = ws.sid
            ? ws.sid
            : req.headers['gun-sid'];
          ws.sub =
            ws.sub ||
            gun.wsp.on('network').event(function(msg) {
              if (!ws || !ws.send || !ws._socket || !ws._socket.writable) {
                return this.off();
              }
              if (
                !msg ||
                (ws.sid && msg.headers && msg.headers['gun-sid'] === ws.sid)
              ) {
                return;
              }
              if (msg && msg.headers) {
                delete msg.headers['ws-rid'];
              }
              // TODO: BUG? ^ What if other peers want to ack? Do they use the ws-rid or a gun declared id?
              try {
                ws.send(Gun.text.ify(msg));
              } catch (e) {
                // FIXME: Throw when not the expected error
                //if (e !== TODO) throw e;
                console.log(
                  'If this error occured during normal operation validate it here',
                  e
                );
              }
            });
          gun.wsp.wire(req, res);
        }
      );
      gun.__.opt.ws.port = gun.__.opt.ws.port || opt.ws.port || port || 80;
    }
    var wsp = (gun.wsp =
      gun.wsp ||
      function(server, auth) {
        gun.wsp.auth = auth;
        if (!server) {
          return gun;
        }
        if (Gun.fns.is(server.address)) {
          if (server.address()) {
            start(server, server.address().port);
            return gun;
          }
        }
        if (Gun.fns.is(server.get) && server.get('port')) {
          start(server, server.get('port'));
          return gun;
        }
        var listen = server.listen;
        server.listen = function(port) {
          var serve = listen.apply(server, arguments);
          start(serve, port, server);
          return serve;
        };
        return gun;
      });
    gun.wsp.on = gun.wsp.on || Gun.on.create();
    gun.wsp.regex = gun.wsp.regex || opt.route || opt.path || /^\/gun/i;
    gun.wsp.poll = gun.wsp.poll || opt.poll || 1;
    gun.wsp.pull = gun.wsp.pull || opt.pull || gun.wsp.poll * 1000;
    gun.wsp.server =
      gun.wsp.server ||
      function(req, res, next) {
        // http
        next = next || function() {};
        if (!req || !res) {
          return next(), false;
        }
        if (!req.url) {
          return next(), false;
        }
        if (!req.method) {
          return next(), false;
        }
        var msg = {};
        msg.url = url.parse(req.url, true);
        if (!gun.wsp.regex.test(msg.url.pathname)) {
          return next(), false;
        } // TODO: BUG! If the option isn't a regex then this will fail!
        if (
          msg.url.pathname
            .replace(gun.wsp.regex, '')
            .slice(0, 3)
            .toLowerCase() === '.js'
        ) {
          res.writeHead(200, { 'Content-Type': 'text/javascript' });
          res.end(
            (gun.wsp.js =
              gun.wsp.js ||
              require('fs').readFileSync(__dirname + '/../gun.js'))
          ); // gun server is caching the gun library for the client
          return true;
        }
        return (
          http(req, res, function(req, res) {
            if (!req) {
              return next();
            }
            var stream,
              cb = (res = require('./jsonp')(req, res));
            if (req.headers && (stream = req.headers['gun-sid'])) {
              stream = (gun.wsp.peers = gun.wsp.peers || {})[stream] = gun.wsp
                .peers[stream] || { sid: stream };
              stream.sub =
                stream.sub ||
                gun.wsp.on('network').event(function(req) {
                  if (!stream) {
                    return this.off();
                  } // self cleans up after itself!
                  if (
                    !req ||
                    (req.headers && req.headers['gun-sid'] === stream.sid)
                  ) {
                    return;
                  }
                  (stream.queue = stream.queue || []).push(req);
                  stream.drain(stream.reply);
                });
              cb = function(r) {
                (r.headers || {}).poll = gun.wsp.poll;
                res(r);
              };
              stream.drain =
                stream.drain ||
                function(res) {
                  if (
                    !res ||
                    !stream ||
                    !stream.queue ||
                    !stream.queue.length
                  ) {
                    return;
                  }
                  res({
                    headers: { 'gun-sid': stream.sid },
                    body: stream.queue
                  });
                  stream.off = setTimeout(function() {
                    stream = null;
                  }, gun.wsp.pull);
                  stream.reply = stream.queue = null;
                  return true;
                };
              clearTimeout(stream.off);
              if (req.headers.pull) {
                if (stream.drain(cb)) {
                  return;
                }
                return (stream.reply = cb);
              }
            }
            gun.wsp.wire(req, cb);
          }),
          true
        );
      };
    if (
      (gun.__.opt.maxSockets = opt.maxSockets || gun.__.opt.maxSockets) !==
      false
    ) {
      require('https').globalAgent.maxSockets = require('http').globalAgent.maxSockets =
        gun.__.opt.maxSockets || Infinity;
    }
    gun.wsp.msg =
      gun.wsp.msg ||
      function(id) {
        if (!id) {
          return (
            (gun.wsp.msg.debounce[(id = Gun.text.random(9))] = Gun.time.is()),
            id
          );
        }
        clearTimeout(gun.wsp.msg.clear);
        gun.wsp.msg.clear = setTimeout(function() {
          var now = Gun.time.is();
          Gun.obj.map(gun.wsp.msg.debounce, function(t, id) {
            if (now - t < 1000 * 60 * 5) {
              return;
            }
            Gun.obj.del(gun.wsp.msg.debounce, id);
          });
        }, 500);
        if ((id = gun.wsp.msg.debounce[id])) {
          return (gun.wsp.msg.debounce[id] = Gun.time.is()), id;
        }
      };
    gun.wsp.msg.debounce = gun.wsp.msg.debounce || {};
    gun.wsp.wire =
      gun.wsp.wire ||
      (function() {
        // all streams, technically PATCH but implemented as PUT or POST, are forwarded to other trusted peers
        // except for the ones that are listed in the message as having already been sending to.
        // all states, implemented with GET, are replied to the source that asked for it.
        function flow(req, res) {
          if (!req.auth || req.headers.broadcast) {
            gun.wsp.on('network').emit(Gun.obj.copy(req));
          }
          if (req.headers.rid) {
            return;
          } // no need to process.
          if (Gun.is.lex(req.body)) {
            return tran.get(req, res);
          } else {
            return tran.put(req, res);
          }
        }
        function tran(req, res) {
          if (!req || !res || !req.body || !req.headers || !req.headers.id) {
            return;
          }
          if (gun.wsp.msg(req.headers.id)) {
            return;
          }
          req.method = req.body && !Gun.is.lex(req.body) ? 'put' : 'get';
          if (gun.wsp.auth) {
            return gun.wsp.auth(
              req,
              function(reply) {
                if (!reply.headers) {
                  reply.headers = {};
                }
                if (!reply.headers['Content-Type']) {
                  reply.headers['Content-Type'] = tran.json;
                }
                if (!reply.rid) {
                  reply.headers.rid = req.headers.id;
                }
                if (!reply.id) {
                  reply.headers.id = gun.wsp.msg();
                }
                res(reply);
              },
              flow
            );
          } else {
            return flow(
              req,
              res
            );
          }
        }
        tran.get = function(req, cb) {
          var key = req.url.key,
            reply = {
              headers: {
                'Content-Type': tran.json,
                rid: req.headers.id,
                id: gun.wsp.msg()
              }
            };
          //Gun.log(req);
          // NTS HACK! SHOULD BE ITS OWN ISOLATED MODULE! //
          if (
            req &&
            req.url &&
            req.url.pathname &&
            req.url.pathname.indexOf('gun.nts') >= 0
          ) {
            return cb({
              headers: reply.headers,
              body: { time: Gun.time.is() }
            });
          }
          // NTS END! SHOULD HAVE BEEN ITS OWN MODULE //
          // ALL HACK! SHOULD BE ITS OWN MODULE OR CORE? //
          if (req && req.url && Gun.obj.has(req.url.query, '*')) {
            return gun.all(req.url.key + req.url.search, function(err, list) {
              cb({
                headers: reply.headers,
                body: err
                  ? err.err
                    ? err
                    : { err: err || 'Unknown error.' }
                  : list || null
              });
            });
          }
          //Gun.log("GET!", req);
          key = req.body;
          //Gun.log("tran.get", key);
          var opt = { key: false, local: true };
          //gun.get(key, function(err, node){
          (gun.__.opt.wire.get ||
            function(key, cb) {
              cb(null, null);
            })(
            key,
            function(err, node) {
              //Gun.log("tran.get", key, "<---", err, node);
              reply.headers.id = gun.wsp.msg();
              if (err || !node) {
                if (opt.on && opt.on.off) {
                  opt.on.off();
                }
                return cb({
                  headers: reply.headers,
                  body: err
                    ? err.err
                      ? err
                      : { err: err || 'Unknown error.' }
                    : null
                });
              }
              if (Gun.obj.empty(node)) {
                if (opt.on && opt.on.off) {
                  opt.on.off();
                }
                return cb({ headers: reply.headers, body: node });
              } // we're out of stuff!
              /*
					(function(chunks){ // FEATURE! Stream chunks if the nodes are large!
						var max = 10, count = 0, soul = Gun.is.node.soul(node);
						if(Object.keys(node).length > max){
							var n = Gun.is.node.soul.ify({}, soul);
							Gun.obj.map(node, function(val, field){
								if(!(++count % max)){
									cb({headers: reply.headers, chunk: n}); // send node chunks
									n = Gun.is.node.soul.ify({}, soul);
								}
								Gun.is.node.state.ify([n, node], field, val);
							});
							if(count % max){ // finish off the last chunk
								cb({headers: reply.headers, chunk: n});
							}
						} else {
							cb({headers: reply.headers, chunk: node}); // send full node
						}
					}([]));
					*/
              cb({ headers: reply.headers, chunk: node }); // Use this if you don't want streaming chunks feature.
            },
            opt
          );
        };
        tran.put = function(req, cb) {
          // NOTE: It is highly recommended you do your own PUT/POSTs through your own API that then saves to gun manually.
          // This will give you much more fine-grain control over security, transactions, and what not.
          var reply = {
            headers: {
              'Content-Type': tran.json,
              rid: req.headers.id,
              id: gun.wsp.msg()
            }
          };
          if (!req.body) {
            return cb({ headers: reply.headers, body: { err: 'No body' } });
          }
          //Gun.log("\n\ntran.put ----------------->", req.body);
          if (Gun.is.graph(req.body)) {
            if (
              (req.err = Gun.union(gun, req.body, function(err, ctx) {
                // TODO: BUG? Probably should give me ctx.graph
                if (err) {
                  return cb({
                    headers: reply.headers,
                    body: { err: err || 'Union failed.' }
                  });
                }
                ctx = ctx || {};
                ctx.graph = {};
                Gun.is.graph(req.body, function(node, soul) {
                  ctx.graph[soul] = gun.__.graph[soul];
                });
                (gun.__.opt.wire.put ||
                  function(g, cb) {
                    cb('No save.');
                  })(
                  ctx.graph,
                  function(err, ok) {
                    if (err) {
                      return cb({
                        headers: reply.headers,
                        body: { err: err || 'Failed.' }
                      });
                    } // TODO: err should already be an error object?
                    cb({
                      headers: reply.headers,
                      body: { ok: ok || 'Persisted.' }
                    });
                    //Gun.log("tran.put <------------------------", ok);
                  },
                  { local: true }
                );
              }).err)
            ) {
              cb({
                headers: reply.headers,
                body: { err: req.err || 'Union failed.' }
              });
            }
          } else {
            cb({ headers: reply.headers, body: { err: 'Not a valid graph!' } });
          }
        };
        // eslint-disable-next-line no-unused-vars
        gun.wsp.on('network').event(function(req) {
          // TODO: MARK! You should move the networking events to here, not in WSS only.
        });
        tran.json = 'application/json';
        return tran;
      })();
    if (opt.server) {
      wsp(opt.server);
    }

    if (gun.wsp.driver) {
      return;
    }
    gun.wsp.driver = {};
    var noop = function() {
      return;
    };
    var get = gun.__.opt.wire.get || noop;
    var put = gun.__.opt.wire.put || noop;
    var driver = {
      put: function(graph, cb, opt) {
        put(graph, cb, opt);
        opt = opt || {};
        if (opt.local) {
          return;
        }
        var id = gun.wsp.msg();
        gun.wsp.on('network').emit({
          // sent to dynamic peers!
          headers: { 'Content-Type': 'application/json', id: id },
          body: graph
        });
        var ropt = { headers: {}, WebSocket: WebSocket };
        ropt.headers.id = id;
        Gun.obj.map(opt.peers || gun.__.opt.peers, function(peer, url) {
          Gun.request(
            url,
            graph,
            function(err, reply) {
              reply.body =
                reply.body || reply.chunk || reply.end || reply.write;
              if (err || !reply || (err = reply.body && reply.body.err)) {
                return cb({ err: Gun.log(err || 'Put failed.') });
              }
              cb(null, reply.body);
            },
            ropt
          );
        });
      },
      get: function(lex, cb, opt) {
        get(lex, cb, opt);
        opt = opt || {};
        if (opt.local) {
          return;
        }
        if (!Gun.request) {
          throw new Error('Server could not find default network abstraction.');
        }
        var ropt = { headers: {} };
        ropt.headers.id = gun.wsp.msg();
        Gun.obj.map(opt.peers || gun.__.opt.peers, function(peer, url) {
          Gun.request(
            url,
            lex,
            function(err, reply) {
              reply.body =
                reply.body || reply.chunk || reply.end || reply.write;
              if (err || !reply || (err = reply.body && reply.body.err)) {
                return cb({ err: Gun.log(err || 'Get failed.') });
              }
              cb(null, reply.body);
            },
            ropt
          );
        });
      }
    };
    var WebSocket = require('ws');
    Gun.request.WebSocket = WebSocket;
    Gun.request.createServer(gun.wsp.wire);
    gun.__.opt.wire = driver;
    gun.opt({ wire: driver }, true);
  });
})({});
